// Copyright (c) 2010 SuccessFactors, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the SuccessFactors, Inc. nor the names of
//       its contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.jxt;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import org.owasp.jxt.compiler.CompileResults;
import org.owasp.jxt.compiler.Compiler;
import org.owasp.jxt.servlet.JxtServletBase;
import org.owasp.jxt.tag.core.CoreNodeFactory;
import org.owasp.jxt.tag.html.HtmlNodeFactory;
import org.owasp.jxt.tag.jsp.JspNodeFactory;
import org.owasp.jxt.tag.jsp.JspTagNodeFactory;
import org.owasp.jxt.webinf.TLDReader;
import org.owasp.jxt.webinf.TagLibraryInfoImpl;
import org.owasp.jxt.webinf.WebApplication;
import org.owasp.jxt.webinf.WebXmlReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * JxtEngine
 *
 * @author Jeffrey Ichnowski
 * @version $Revision: 8 $
 */
public final class JxtEngine {

    private static final Logger _log = LoggerFactory.getLogger(JxtEngine.class);
    private static final String VERSION = "0.1";

    public static final String XHTML_NSURI = "http://www.w3.org/1999/xhtml";

    /** The XML Namespace URI for JXT Core elements. */
    public static final String JXT_NSURI = "http://www.owasp.org/2010/jxt";

    /** The old XML Namespace URI for JXT Core elements.  Here for
     * backwards compatibility.  New templates should use
     * JXT_NSURI.  */
    public static final String JXT_NSURI_OLD = "http://developer.successfactors.com/2009/jxt";
    public static final String JSP_NSURI = "http://java.sun.com/JSP/Page";

    public static final String JXT_PACKAGE = "org.owasp.jxt.work";

    public static final String FILE_EXTENSION = ".jxt";

    private URL _webRoot;
    private File _tempDir;
    private File _javaDir;
    private File _classDir;

    private WebApplication _webApp;

    /**
     * Maps XML Namespace URIs to JxtNodeFactorys.
     */
    private Map<String,JxtNodeFactory> _nsUriToFactory = new HashMap<String,JxtNodeFactory>();

    /**
     * Maps XML namespace URIs to JSP Tag node factories.
     */
    private QuickCache<URL,JspTagNodeFactory> _jspNodeFactoryCache = new QuickCache<URL,JspTagNodeFactory>() {
        @Override
        protected JspTagNodeFactory resolve(URL tldFile) {
            return loadTldImpl(tldFile);
        }
    };

    // private JxtLoader _classLoader;
    // private Set<String> _loadedClasses = new HashSet<String>();

    private String _baseClassPath;
    private ClassLoader _baseClassLoader;
    private Compiler _compiler;
    private URLEntityResolver _entityResolver = new URLEntityResolver();

    /** Private constructor.  Instance creation is through static
     * factory methods. */
    private JxtEngine(final File tempRoot, final File classDir, final Compiler compiler) {
        // TODO: classDir can be specified indepentently on both the
        // compiler and the JxtEngine.  This should be cleaned up so
        // that only one place needs to be specified.

        CoreNodeFactory coreNodeFactory = new CoreNodeFactory();
        _nsUriToFactory.put(JXT_NSURI, coreNodeFactory);
        _nsUriToFactory.put(JXT_NSURI_OLD, coreNodeFactory);
        _nsUriToFactory.put(XHTML_NSURI, new HtmlNodeFactory());
        _nsUriToFactory.put(JSP_NSURI, new JspNodeFactory());

        // TODO: track exactly which files were created and delete
        // them in reverse order.
        Runtime.getRuntime().addShutdownHook(new Thread("JxtEngine temp file cleanup") {
            void delete(File f) {
                if (f.isDirectory()) {
                    for (File c : f.listFiles()) {
                        delete(c);
                    }
                }
                f.delete();
            }
            public void run() {
                if (_tempDir != null) {
                    _log.debug("Cleaning up temporary JXT files in {}", _tempDir);
                    delete(_tempDir);
                }
            }
        });

        try {
            File file = File.createTempFile("jxt", "", tempRoot);
            if (file.exists()) {
                file.delete();
            }

            if (file.mkdir()) {
                _log.debug("Using {} as temp directory", file);
                _tempDir = file;
            }

            File javaDir = _tempDir;
            for (String part : JXT_PACKAGE.split("\\.")) {
                javaDir = new File(javaDir, part);
            }

            javaDir.mkdirs();
            _javaDir = javaDir;
        } catch (IOException e) {
            // TODO! clean up
            _log.error("Failed to allocate temporary directory", e);
        }

        if (classDir != null) {
            classDir.mkdirs();
        }

        _classDir = (classDir == null) ? _tempDir : classDir;

        // initClassLoader();

//         StringBuilder classPath = new StringBuilder();
//         classPath.append(System.getProperty("java.class.path"));
//         classPath.append(System.getProperty("path.separator"));
//         classPath.append(new File(_webInf, "classes"));

        calcClassPath();

        _compiler = compiler;
        _compiler.setDestDir(_classDir);
        _compiler.setSourceDirectory(_tempDir);
        _compiler.setClassPath(_baseClassPath);
        _compiler.setClassLoader(_baseClassLoader);
    }

    /**
     * Returns the version of the JxtEngine.
     *
     * @return version.
     */
    public String getVersion() {
        return VERSION;
    }

    /**
     * Creates a ClassLoader used for loading JXT files.
     *
     * @return a new class loader
     */
    private JxtLoader createClassLoader() {
        try {
            return new JxtLoader(
                new URL[] { _classDir.toURI().toURL() },
                Thread.currentThread().getContextClassLoader());
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException(
                "MalformedURLException", e);
        }
    }

    /**
     * Sets the class path used for compiling files.  This must be in
     * standard classpath (OS-dependent) form.
     *
     * @param classPath the class path to use
     */
    private void setClassPath(String classPath) {
        String sep = System.getProperty("path.separator");

        _compiler.setClassPath(_baseClassPath + sep + classPath);
    }

    /**
     * Calculates the classpath by walking up the ClassLoader chain
     * and extracting URLs from URLClassLoaders.
     */
    void calcClassPath() {
        StringBuilder cp = new StringBuilder();
        cp.append(System.getProperty("java.class.path"));

        String sep = System.getProperty("path.separator");

        ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
        if (parentClassLoader == null) {
            parentClassLoader = getClass().getClassLoader();
        }

        _baseClassLoader = parentClassLoader;

        while (parentClassLoader != null) {
            if (parentClassLoader instanceof URLClassLoader) {
                for (URL url : ((URLClassLoader)parentClassLoader).getURLs()) {
                    if ("file".equals(url.getProtocol())) {
                        cp.append(sep).append(url.getFile());
                    }
                }
            }
            parentClassLoader = parentClassLoader.getParent();
        }

        _baseClassPath = cp.toString();
    }

    /**
     * Creates a new JxtEngine instance.  NOTE: usually it is best to
     * have one JxtEngine per web-root.
     *
     * @return a new JxtEngine instance.
     */
    public static JxtEngine newInstance() {
        try {
            return builder().build();
        } catch (JxtConfigException e) {
            throw (AssertionError)
                new AssertionError("JxtConfigException should never be thrown here").
                initCause(e);
        }
    }

    /**
     * Fluent builder for creating a new JxtEngine.  To create a new
     * JxtEngine instance, use:
     *
     * <pre>
     *     JxtEngine engine = JxtEngine.builder().classpath("/").tempDir("/tmp").build();
     * </pre>
     *
     * @return a builder for creating a JxtEngine
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Sets the web root.  The WEB-INF/web.xml and tag tlds will be
     * parsed when this method is called.
     */
    private void setWebRoot(File webroot)
        throws JxtConfigException
    {
        try {
            setWebRoot(webroot.toURI().toURL());
        } catch (MalformedURLException e) {
            // This should not happen
            throw new UnsupportedOperationException(
                "MalformedURLException", e);
        }
    }

    /**
     * Sets the web root.  The WEB-INF/web.xml and tag tlds will be
     * parsed when this method is called.
     */
    private void setWebRoot(URL webroot)
        throws JxtConfigException
    {
        _webRoot = webroot;

        _entityResolver.setRoot(_webRoot);

        URL webXml;

        try {
            URL webInf = new URL(webroot, "WEB-INF/");
            webXml = new URL(webInf, "web.xml");
        } catch (MalformedURLException e) {
            throw new JxtConfigException("MalformedURLException", e);
        }

        try {
            // TODO: according to spec, the WEB-INF directory and
            // subdirectories will be scanned for TLD files.

            InputStream in = webXml.openStream();
            try {
                _webApp = WebXmlReader.parse(in);
            } finally {
                in.close();
            }
        } catch (IOException e) {
            throw new JxtConfigException("Error reading "+webXml, e);
        } catch (ParserConfigurationException e) {
            throw new JxtConfigException("Error reading "+webXml, e);
        } catch (SAXException e) {
            throw new JxtConfigException("Error reading "+webXml, e);
        }

        _log.debug("Loaded {}: {}", webXml, _webApp);
    }

    /**
     * Loads a tag library definition (TLD) file from a given
     * location.
     *
     * @param location the location of the TLD
     * @return the node factory or null
     */
    private JxtNodeFactory loadTld(String location) {
        // TODO: can location contain '..'?  Is that bad?

        if (location.startsWith("/")) {
            location = location.substring(1);
        }

        URL tldFile;

        try {
            tldFile = new URL(_webRoot, location);
        } catch (MalformedURLException e) {
            throw new UnsupportedOperationException(
                "MalformedURLException", e);
        }

        return _jspNodeFactoryCache.get(tldFile);
    }

    /**
     * Actual loading of the tag library definition.  The QuickCache
     * resolve method calls this when resolving the TLD the first
     * time.
     *
     * @param tldFile the file that contains the TLD
     * @return the node factory or null
     */
    JspTagNodeFactory loadTldImpl(URL tldFile) {
        _log.debug("Loading TLD in {}", tldFile);

        TagLibraryInfoImpl tld;

        try {
            InputStream in = tldFile.openStream();
            try {
                tld = TLDReader.parse(in);
            } finally {
                in.close();
            }
        } catch (FileNotFoundException e) {
            _log.warn("File Not Found when resolving TLD: "+tldFile);
            return null;
        } catch (IOException e) {
            _log.warn("IOException reading "+tldFile, e);
            return null;
        } catch (ParserConfigurationException e) {
            _log.warn("Exception reading "+tldFile, e);
            return null;
        } catch (SAXException e) {
            _log.warn("Exception reading "+tldFile, e);
            return null;
        }

        List<String> loadErrors = new ArrayList<String>();
        tld.load(loadErrors);

        if (!loadErrors.isEmpty()) {
            _log.warn("Error loading TLD {}", tldFile);
            for (String err : loadErrors) {
                _log.warn("--> "+err);
            }
        }

        return new JspTagNodeFactory(tld);
    }

    /**
     * Called by JxtParser to resolve a JxtNodeFactory for a given
     * namespace URI.  This method first checks for registered
     * factories.  It then checks for JSP tag libraries.
     *
     * @param nsUri the namespace URI
     * @return the JxtNodeFactory or null
     */
    JxtNodeFactory getFactoryForNS(String nsUri) {
        JxtNodeFactory factory = _nsUriToFactory.get(nsUri);

        // TODO: consider caching JSP tag node factories in the
        // _nsUriToFactory map.  Currently they are cached in the
        // _jspNodeFactoryCache, but additional processing is needed
        // for each request.

        if (factory == null && _webApp != null) {
            // First try the web.xml mapping of URI to location
            String location = _webApp.getTaglibLocation(nsUri);
            if (location != null) {
                factory = loadTld(location);
            }

            // If that failed, try the URI as a file location
            if (factory == null) {
                if (nsUri.startsWith("/")) {
                    factory = loadTld(nsUri);
                }
            }
        }

        return factory;
    }

    /**
     * Utility method to turn a source file name into a valid java
     * class name.
     *
     * @param source the source file name
     * @return the class name
     */
    private static String makeClassName(String source) {
        StringBuilder buf = new StringBuilder();
        for (int i=0, n=source.length() ; i<n ; ++i) {
            char ch = source.charAt(i);
            if ('a' <= ch && ch <= 'z' ||
                'A' <= ch && ch <= 'Z' ||
                '0' <= ch && ch <= '9' && i > 0)
            {
                buf.append(ch);
            } else if (ch == '_') {
                buf.append("__");
            } else if (ch <= '\u00ff') {
                buf.append('_')
                    .append(String.format("%02x", (int)ch));
            } else {
                buf.append('_')
                    .append(String.format("%04X", (int)ch));
            }
        }

        return buf.toString();
    }

    /**
     * Compiles a JXT file into a Java Servlet class.
     *
     * @param file the name of the JXT file (relative to the web root)
     * @return The compiled class (and related information)
     * @throws CompileException if the JXT compilation failed for any
     * reason.
     */
    @SuppressWarnings("unchecked")
    public <C> JxtCompilation<JxtTemplate<C>> compileTemplate(String file, Class<C> contextType)
        throws CompileException
    {
        JxtCompilation rv = compileImpl(file, JxtTemplate.class);
        return (JxtCompilation<JxtTemplate<C>>)rv;
    }

    /**
     * Compiles a JXT file into a Java Servlet class.
     *
     * @param file the name of the JXT file (relative to the web root)
     * @return The compiled class (and related information)
     * @throws CompileException if the JXT compilation failed for any
     * reason.
     */
    public JxtCompilation<JxtServletBase> compileServlet(String file) throws CompileException {
        return compileImpl(file, JxtServletBase.class);
    }

    /**
     * Compiles a JXT file into a Java class.
     *
     * @param file the name of the JXT file (relative to the web root)
     * @param clazz the class the template extends (must be one of
     * JxtServletBase or JxtTemplate)
     * @return The compiled class (and related information)
     * @throws CompileException if the JXT compilation failed for any
     * reason.
     */
    private <U> JxtCompilation<U> compileImpl(String file, Class<U> clazz) throws CompileException {
        JxtNode root;

        // Files should always be relative to the root.  We normalize
        // it to remove ".."s and related, and then strip out the
        // leading "/" make sure.
        file = JxtUtils.resolveRelative(null, file).replaceFirst("^/+", "");

        TrackingEntityResolver er = new TrackingEntityResolver(_entityResolver);
        ValidationContext vc = new ValidationContext();

        try {
            InputSource in = er.resolveEntity(null, file);
            root = JxtParser.parse(this, er, in, vc);
        } catch (ParserConfigurationException e) {
            throw new CompileException("configuration error", e, er.getEntitiesResolved());
        } catch (SAXParseException e) {
            throw new CompileException(
                e.getSystemId()+":"+e.getLineNumber()+":"+e.getColumnNumber()+": "+e.getMessage(),
                e, er.getEntitiesResolved());
        } catch (SAXException e) {
            throw new CompileException(file+": "+e, e, er.getEntitiesResolved());
        } catch (IOException e) {
            throw new CompileException(file+": "+e, e, er.getEntitiesResolved());
        }

        if (!(root instanceof JxtRoot)) {
            vc.error(root, "validation.invalid-root");
        }

        root.validate(vc);

        if (!vc.isValid()) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            for (LocatedMessage error : vc.getErrors()) {
                pw.println(error);
            }
            pw.close();
            throw new CompileException(sw.toString(), er.getEntitiesResolved());
        }

        String targetClassName = makeClassName(file);

        File targetJavaFile = new File(
            _javaDir, targetClassName + ".java");

        _log.debug("Creating {}", targetJavaFile);

        try {
            FileOutputStream fileOut = new FileOutputStream(targetJavaFile);
            try {
                PrintWriter out = new PrintWriter(
                    new OutputStreamWriter(fileOut, "UTF-8"));

                TranslateContext rc = new TranslateContext(
                    this,
                    (JxtRoot)root,
                    file.toString(),
                    targetClassName,
                    er.getEntitiesResolved(),
                    out);

                root.translate(rc);

                rc.finish();
            } finally {
                fileOut.close();
            }
        } catch (IOException e) {
            throw new CompileException(
                "IOException writing "+targetJavaFile, e, er.getEntitiesResolved());
        }

        String fqcn = JXT_PACKAGE + "." + targetClassName;

        CompileResults results = _compiler.compile(fqcn); //targetJavaFile);

        if (!results.isSuccess()) {
            throw new CompileException(
                results.getDetailsAsString(), er.getEntitiesResolved());
        }

        try {
            Class<? extends U> templateClass =
                createClassLoader().loadClass(fqcn).asSubclass(clazz);

            return new JxtCompilation<U>(
                file,
                er.getEntitiesResolved(),
                templateClass,
                vc.getWarnings());
        } catch (ClassNotFoundException e) {
            throw new CompileException("compile failed", e, er.getEntitiesResolved());
        } catch (ClassCastException e) {
            throw new CompileException("unexpected class", e, er.getEntitiesResolved());
        }
    }


    public static void main(String[] args) throws Exception {
        List<String> files = new ArrayList<String>();
        String webRoot = null;

        int state = 0;

        for (String arg : args) {
            switch (state) {
            case 0:
                if ("--web-root".equals(arg)) {
                    state = 1;
                } else {
                    files.add(arg);
                }
                break;
            case 1:
                webRoot = arg;
                state = 0;
                break;
            default:
                throw new IllegalStateException();
            }
        }

        JxtEngine engine = JxtEngine.builder()
            .webRoot(new File(webRoot))
            .build();

        for (String file : files) {
            engine.compileServlet(file);
//             InputStream in = new FileInputStream(file);
//             try {
//                 JxtNode root = JxtParser.parse(engine, in);

//                 ValidationContext vctx = new ValidationContext();

//                 root.validate(vctx);

//                 PrintWriter out = new PrintWriter(
//                     new OutputStreamWriter(System.out), true);

//                 root.translate(new TranslateContext(file, out));
//             } catch (SAXParseException e) {
//                 System.err.println("Parse Error: "+e+" in "+file+":"+
//                                    e.getLineNumber()+":"+e.getColumnNumber());
//             } finally {
//                 in.close();
//             }
        }
    }

    /**
     * Fluent builder for creating JxtEngine instances.
     */
    public static final class Builder {
        private String _classPath;
        private File _webRoot;
        private File _tempDir;
        private File _classDir;
        private Compiler _compiler;

        /** Prevent construction outside of the builder() method. */
        private Builder() {}

        public JxtEngine build() throws JxtConfigException {
            Compiler compiler = _compiler;

            if (compiler == null) {
                compiler = Compiler.newInstance();
            }

            JxtEngine engine = new JxtEngine(_tempDir, _classDir, compiler);

            if (_webRoot != null) {
                engine.setWebRoot(_webRoot);
            }

            if (_classPath != null) {
                engine.setClassPath(_classPath);
            }

            return engine;
        }

        /**
         * Sets the class path used for compiling files.  This must be
         * in standard classpath (OS-dependent) form.
         *
         * @param classPath the class path to use
         * @return this
         */
        public Builder classPath(final String classPath) {
            _classPath = classPath;
            return this;
        }

        /**
         * Sets the web root.  The WEB-INF/web.xml and tag tlds will be
         * parsed when this method is called.
         *
         * @param webRoot the webroot
         * @return this
         */
        public Builder webRoot(final File webRoot) {
            _webRoot = webRoot;
            return this;
        }

        /**
         * Sets the temporary directory to use for intermediate source
         * and class files.  If not set, or set to null, a temporary
         * file will be obtained from the File API.
         *
         * @param tempDir the tenp dir
         * @return this
         */
        public Builder tempDir(final File tempDir) {
            _tempDir = tempDir;
            return this;
        }

        /**
         * Sets the destination directory where class files will be
         * written.  If not specified, the tempDir will be used.
         * Also, in the future, if not specified, class files may not
         * be written to the file system at all.
         *
         * @param classDir the class directory
         * @return this
         */
        public Builder classDir(final File classDir) {
            _classDir = classDir;
            return this;
        }

        /**
         * Specify which compiler to use.
         *
         * @param compiler -- the compiler to use.
         */
        public Builder compiler(final Compiler compiler) {
            _compiler = compiler;
            return this;
        }
    }

} // JxtEngine
